在Java基本类型对应的包装类型中,最为复杂的就是字符类型和字符串类型了。本篇在讲解字符类型之前,必须要讲解一下Unicode编码方面的知识,否则不好理解源代码。
1、Unicode编码基本概念
(1)编码字符集
编码字符集是一个字符集,它为每一个字符分配一个唯一数字。Unicode 标准的核心是一个编码字符集,字母“A”的编码为0041和字符“€”的编码为20AC。Unicode标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。
(2)代码点code point和代码单元
代码点是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点。有效的 Unicode代码点范围是 U+0000 至 U+10FFFF
代码单元可以理解为字符编码的一个基本单元,最常用的代码单元是字节(即8位),但是16位和32位整数也可以用于内部处理。
(3)增补字符
16 位编码的所有 65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的16 位限制的字符被称作增补字符。
Java的char类型是固定16bits的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。
因此,Java 平台不仅需要支持增补字符,而且必须使应用程序能够方便地做到这一点。Java Community Process 召集了一个专家组,以期找到一个适当的解决方案。该小组被称为JSR-204专家组,使用Unicode 增补字符支持的Java技术规范请求的编号。
增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane )。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符。
2、基于Unicode的具体编码格式
UTF-32 即将每一个 Unicode 代码点表示为相同值的32位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。
UTF-16 使用一个或两个未分配的16位代码单元的序列对 Unicode 代码点进行编码。假设U是一个代码点,也就是Unicode编码表中一个字符所对应的Unicode值。
(1) 如果在BMP级别中,那么16bits(一个代码单元)就足够表示出字符的Unicode值。
(2) 如果U+10FFFF>U>=U+10000,也就是处于增补字符级别中。UTF-16用2个16位来表示出了,并且正好将每个16位都控制在替代区域U+D800-U+DFFF(其中\uD800-\uDBFF为高代理项 范围,\uDC00- \uDFFF为低代理项 范围) 中。
下面来看一下Java是如何处理这些增补字符的。
分别初始化2个16位无符号的整数 —— W1和W2。其中W1=110110yyyyyyyyyy(0xD800-0xDBFF),W2 = 110111xxxxxxxxxx(0xDC00-OxDFFF)。然后,将Unicode的高10位分配给W1的低10位,将Unicode 的低10位分配给W2的低10位。这样就可以将20bits的代码点U拆成两个16bits的代码单元。而且这两个代码点正好落在替代区域U+D800-U+DFFF中。
举个例子:代码点U+1D56B(使用4个字节表示的代码点)
0x1D56B= 0001 1101 01-01 0110 1011
将0x1D56B的高10位0001 1101 01分配给W1的低10位组合成110110 0001 1101 01=0xD875
将0x1D56B的低10位01 0110 1011分配给W2的低10位组合成110111 01 0110 1011=0xDD6B
这样代码点U+1D56B采用UTF-16编码方式,用2个连续的代码单元U+D875和U+DD68表示出了。
在Charachter类中定义相关的变量如下:
[java] view plaincopyprint?

- public static final char MIN_HIGH_SURROGATE = '\uD800';
- public static final char MAX_HIGH_SURROGATE = '\uDBFF';
- public static final char MIN_LOW_SURROGATE = '\uDC00';
- public static final char MAX_LOW_SURROGATE = '\uDFFF';
- public static final char MIN_SURROGATE = MIN_HIGH_SURROGATE; // min_surrogate '\uD800'
- public static final char MAX_SURROGATE = MAX_LOW_SURROGATE; // max_surrogate '\uDFFF'
- public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;
- public static final int MIN_CODE_POINT = 0x000000;
- public static final int MAX_CODE_POINT = 0X10FFFF;
利用如上定义的一些常量,就可以对传入的代码点或字符进行判断了,如下:
[java] view plaincopyprint?

- // 最大的数为:10FFFF,也就是说以10开头的一定为Unicode编码
- public static boolean isValidCodePoint(int codePoint) {
- // Optimized form of:
- // codePoint >= MIN_CODE_POINT && codePoint <= MAX_CODE_POINT
- int plane = codePoint >>> 16;
- return plane < ((MAX_CODE_POINT + 1) >>> 16);
- }
- public static boolean isBmpCodePoint(int codePoint) { // 是否为BMP代码点
- return codePoint >>> 16 == 0;//
- // Optimized form of:
- // codePoint >= MIN_VALUE && codePoint <= MAX_VALUE
- // We consistently use logical shift (>>>) to facilitate(促进)
- // additional runtime optimizations.
- }
- public static boolean isSupplementaryCodePoint(int codePoint) { // 是否为增补字符代码点
- return codePoint >= MIN_SUPPLEMENTARY_CODE_POINT && codePoint < MAX_CODE_POINT + 1;
- }
- public static boolean isHighSurrogate(char ch) {// 是否为高代理项
- // Help VM constant-fold; MAX_HIGH_SURROGATE + 1 == MIN_LOW_SURROGATE
- return ch >= MIN_HIGH_SURROGATE && ch < (MAX_HIGH_SURROGATE + 1);
- }
- public static boolean isLowSurrogate(char ch) {// 是否为低代理项
- return ch >= MIN_LOW_SURROGATE && ch < (MAX_LOW_SURROGATE + 1);
- }
- public static boolean isSurrogate(char ch) { // 是否为代理项
- return ch >= MIN_SURROGATE && ch < (MAX_SURROGATE + 1);
- }
如上只是介绍了一些基本的方法,这种类似的方法还很多,如
[java] view plaincopyprint?

- <span style="font-family: 'Courier New';">public static boolean isSurrogatePair(char high, char low) {
- return isHighSurrogate(high) && isLowSurrogate(low);
- }
- // 计算一个字符需要用几个单元块来表示
- public static int charCount(int codePoint) {
- return codePoint >= MIN_SUPPLEMENTARY_CODE_POINT ? 2 : 1;
- }
- // 根据高和低代理项计算增补字符代码点
- public static int toCodePoint(char high, char low) {
- // Optimized form of:
- // return ((high - MIN_HIGH_SURROGATE) << 10)
- // + (low - MIN_LOW_SURROGATE)
- // + MIN_SUPPLEMENTARY_CODE_POINT;
- return ((high << 10) + low) + (MIN_SUPPLEMENTARY_CODE_POINT - (MIN_HIGH_SURROGATE << 10) - MIN_LOW_SURROGATE);//???
- }
- public static int codePointAt(CharSequence seq, int index) {
- char c1 = seq.charAt(index++);
- if (isHighSurrogate(c1)) {
- if (index < seq.length()) {
- char c2 = seq.charAt(index);
- if (isLowSurrogate(c2)) {
- return toCodePoint(c1, c2);
- }
- }
- }
- return c1;
- }
- </span>
然后使用字符常量对一些字符和代码点进行了判断,如判断是否为合法的高位和低位增补字符、Unicode代码点需要几个单元块来存储、从字符序列中取出增补字符的代码点等。
UTF-8 使用一至四个字节的序列对编码 Unicode 代码点进行编码。U+0000 至 U+007F 使用一个字节编码,U+0080 至 U+07FF 使用两个字节,U+0800 至 U+FFFF 使用三个字节,而 U+10000 至 U+10FFFF 使用四个字节。UTF-8 设计原理为:字节值 0x00 至 0x7F 始终表示代码点 U+0000 至 U+007F(Basic Latin 字符子集,它对应 ASCII 字符集)。这些字节值永远不会表示其他代码点,这一特性使 UTF-8 可以很方便地在软件中将特殊的含义赋予某些 ASCII 字符。
以下是Unicode和UTF-8之间的转换关系表:
U-00000000 - U-0000007F: 0xxxxxxx
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxx x 10xxxxxx
U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
可以看到:
(1)如果一个字节以10开头,一定不是首字节,需要向前查找。
(2)在一个首字节中,如果以0开头,表示是一个ASCII字符,而开头的连续的1的个数也表示了这个字符的字节数。如1110xxxx表示这个字符由三个字节组成。
下面来看一个使用各种编码对字符进行编码的例子,如下:

3、源代码相关API
在使用源码相关的API时,需要注意(摘自Java 7 API说明文档):
- The methods that only accept a
char
value cannot support supplementary characters. They treat char
values from the surrogate ranges as undefined characters. For example, Character.isLetter('\uD840')
returns false
, even though this specific value if followed by any low-surrogate value in a string would represent a letter.
- The methods that accept an
int
value support all Unicode characters, including supplementary characters. For example, Character.isLetter(0x2F81A)
returns true
because the code point value represents a letter (a CJK ideograph).
也就是说,在使用以char为参数的一类方法时,这类方法并不支持增补字符。参数为整数,也就是支持代码点值做为参数时是支持增补字符的。
4、字符的属性
我们在编写Java代码的过程中,如果要定义一个Java合法的标识符或者需要将一个字符串转换为数值,那么Java是怎么来确定标识符合法或者字符串合法的呢?这就要涉及到字符属性的概念了。先来看CharacterData类的源代码,如下:
[java] view plaincopyprint?

- abstract class CharacterData {
- abstract int getProperties(int ch);
- abstract int getType(int ch);
- abstract boolean isWhitespace(int ch);
- abstract boolean isMirrored(int ch);
- abstract boolean isJavaIdentifierStart(int ch);
- abstract boolean isJavaIdentifierPart(int ch);
- abstract boolean isUnicodeIdentifierStart(int ch);
- abstract boolean isUnicodeIdentifierPart(int ch);
- abstract boolean isIdentifierIgnorable(int ch);
- abstract int toLowerCase(int ch);
- abstract int toUpperCase(int ch);
- abstract int toTitleCase(int ch);
- abstract int digit(int ch, int radix);
- abstract int getNumericValue(int ch);
- abstract byte getDirectionality(int ch);
- //need to implement for JSR204
- int toUpperCaseEx(int ch) {
- return toUpperCase(ch);
- }
- char[] toUpperCaseCharArray(int ch) {
- return null;
- }
- boolean isOtherLowercase(int ch) {
- return false;
- }
- boolean isOtherUppercase(int ch) {
- return false;
- }
- boolean isOtherAlphabetic(int ch) {
- return false;
- }
- boolean isIdeographic(int ch) {
- return false;
- }
- // Character <= 0xff (basic latin) is handled by internal fast-path
- // to avoid initializing large tables.
- // Note: performance of this "fast-path" code may be sub-optimal
- // in negative cases for some accessors due to complicated ranges.
- // Should revisit after optimization of table initialization.
- static final CharacterData of(int ch) {
- if (ch >>> 8 == 0) { // fast-path
- return CharacterDataLatin1.instance; // 使用Latin编码所能表示的字符
- } else {
- switch(ch >>> 16) { //plane 00-16
- case(0):
- return CharacterData00.instance;// 是由两个字节表示的字符,以下全部为增补字符
- case(1):
- return CharacterData01.instance;
- case(2):
- return CharacterData02.instance;
- case(14):
- return CharacterData0E.instance;
- case(15): // Private Use
- case(16): // Private Use
- return CharacterDataPrivateUse.instance;
- default:
- return CharacterDataUndefined.instance;
- }
- }
- }
- }
如上是一个抽象类,这个抽象类中定义了许多判断字符属性的抽象方法,这些方法的具体实现都在Character0X类中,这些类都继承了这个抽象类。其实Character类中有许多对应的方法,并不是Character类继承了这个抽象类,而是通过调用抽象类的具体实现类方法来实现字符属性的判断。我们并不关心这个字符由哪个具体类中的方法来判断,如果以后还增加了一些增补字符,那么只需要实现抽象类并且稍加修改of()方法即可。这就是使用策略模式的好处。
由于抽象类中定义的抽象方法都不是公开的,所以我们只好利用Character类中提供的方法进行字符属性的判断,如下:
[java] view plaincopyprint?

- System.out.println(Character.getDirectionality('('));//13
- System.out.println(Character.getDirectionality('{'));//13
- System.out.println(Character.isMirrored('['));//true
- System.out.println(Character.isMirrored(']'));//true
- System.out.println(Character.isMirrored('c'));//false
- System.out.println(Character.isMirrored('&'));//false
- System.out.println(Character.getNumericValue('z'));//35
- System.out.println(Character.getNumericValue('中'));//-1
- System.out.println(Character.toUpperCase('A'));//A
- System.out.println(Character.digit('c', 16));//12 c在十六进制中代表12
- System.out.println(Character.isJavaIdentifierStart('$'));//true 可以做为Java一个标识符的开头
- System.out.println(Character.isJavaIdentifierStart('&'));//false
- System.out.println(Character.isJavaIdentifierStart('4'));//true 关键字不允许以数字开头
- System.out.println(Character.isJavaIdentifierStart('解'));//true
- System.out.println(Character.isJavaIdentifierPart(' '));//false 可以看到一个Java标识符的定义是不允许有空格的
- System.out.println(Character.isJavaIdentifierPart('4'));//true 关键字不允许以数字开头
- System.out.println(Character.isJavaIdentifierPart('解'));//true
那么Java是怎么判断这些字符的属性的呢?其实每一个Java字符都用一个32位,也就是4个字节来表示这个属性。具体的32位中的各个位所代表的涵义如下:
[java] view plaincopyprint?

- 1 bit mirrored property
- 4 bits directionality property
- 9 bits signed offset used for converting case(有符号偏移,用于转换)
- 1 bit if 1, adding the signed offset converts the character to lowercase
- 1 bit if 1, subtracting(减去) the signed offset converts the character to uppercase
- 1 bit if 1, this character has a titlecase equivalent (possibly itself)
- 3 bits 0 may not be part of an identifier
- 1 ignorable control; may continue a Unicode identifier or Java identifier
- 2 may continue a Java identifier but not a Unicode identifier (unused)
- 3 may continue a Unicode identifier or Java identifier
- 4 is a Java whitespace character
- 5 may start or continue a Java identifier;
- may continue but not start a Unicode identifier (underscores)
- 6 may start or continue a Java identifier but not a Unicode identifier ($)
- 7 may start or continue a Unicode identifier or Java identifier
- Thus:(因此)
- 5, 6, 7 may start a Java identifier(Java标识符)
- 1, 2, 3, 5, 6, 7 may continue a Java identifier
- 7 may start a Unicode identifier
- 1, 3, 5, 7 may continue a Unicode identifier
- 1 is ignorable within an identifier
- 4 is Java whitespace
- 2 bits 0 this character has no numeric property
- 1 adding the digit offset to the character code and then
- masking with 0x1F will produce the desired numeric value
- 2 this character has a "strange" numeric value
- 3 a Java supradecimal digit: adding the digit offset to the
- character code, then masking with 0x1F, then adding 10
- will produce the desired numeric value
- 5 bits digit offset
- 5 bits character type
当我们传入一个'0'字符时,实际上通过
[java] view plaincopyprint?

- static final CharacterData of(int ch)
方法判断后,最终会调用CharacterDataLatin1类中对应的方法去处理。Latin1中的所有字符都会在CharacterDataLatin1类中进行处理,而对于其他的字符,可能会在不同的CharacterDataXX.java类中进行处理,如可以用两个字节表示的字符用CharacterData01类处理,而一些增补字符会用其他的一些类进行处理。
读者如果不是很理解的话,可以查看一下Unicode字符编码分布表,你就会明白CharacterData这个方法了。
具体来看一下ChatacterDataLation1的实现代码:
[java] view plaincopyprint?

- static final int A[] = new int[256];// 2^8=256
- static final String A_DATA =
- "\u4800\u100F\u4800\u100F\u4800\u100F\u4800\u100F\u4800\u100F\u4800\u100F\u4800"+
- "\u100F\u4800\u100F\u4800\u100F\u5800\u400F\u5000\u400F\u5800\u400F\u6000\u400F"+
- // 省略....
- "\u061D\u7002";
- static {
- { // THIS CODE WAS AUTOMATICALLY CREATED BY GenerateCharacter:
- char[] data = A_DATA.toCharArray();
- assert (data.length == (256 * 2));
- int i = 0, j = 0;
- while (i < (256 * 2)) {
- int entry = data[i++] << 16;
- A[j++] = entry | data[i++];
- }
- }
- }
首先还需要说一下Latin1编码。Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。
ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
ISO-8859-1收录的字符除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在ISO-8859-1当中。
因为ISO-8859-1编码范围使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。ASCII编码是一个7位的容器,ISO-8859-1编码是一个8位的容器。
回到我们的源代码中,可以看到最终A[]中存储了256个整数,就是使用有4个字节,32bits来存储的数,但是不能将这256个数当作一个整数来看待,没有任何的意义,需要读取32个比特位中特定的位的值,因为他代表着字符的属性。举个例子:ASCII表中的49代表'0'字符,获取这个字符对应的属性值为A[49],转换后的二进制值如下:
0- 0011-000 000000-0-0 0-011-01-10 000-01001
1位:0表示没有mirrored property,如果是'(','[',这些字符,这个位置的值为1
4位:3
9位:无偏移
1位:无小写
1位:无大写
1位:无首字母大写属性
3位:3 表示是一个合法的Unicode标识符或Java标识符
2位:1 有数字的属性
5位:数字移位为0
5位:字符类型代表的值为9
既然能够得到每个字符的代表属性的整数,接下来当然就是编写方法取出特定二进制位上的值了。如要查看一个字符的类型,而这个类型由二进制位的最后5位表示,取出后5位的方法如下:
[java] view plaincopyprint?

- int getPropertiesEx(int ch) {
- char offset = (char)ch;
- int props = B[offset];
- return props;
- }
- int getType(int ch) {
- int props = getProperties(ch);
- return (props & 0x1F);// 0001 1111取后5bits代表了character type
- }
当然还有许多的方法,有兴趣的读者可以自己去看一下。
本文固定链接: http://www.devba.com/index.php/archives/4777.html | 开发吧